/**
 * Copyright (c) 2009 Severin Smith
 *
 * This file is part of a library called The MidiBus (themidibus) - http://www.smallbutdigital.com/themidibus.php.
 *
 * The MidiBus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version. * The MidiBus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the MidiBus. If not, see <http://www.gnu.org/licenses/>.
 */

package themidibus;

import im.composer.midi.remote.RemoteRawMidiListener;

import java.rmi.RemoteException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.Vector;
import java.util.logging.Logger;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Transmitter;

import themidibus.listener.ChannelBasedMidiListener;
import themidibus.listener.MidiListener;
import themidibus.listener.RawMidiListener;
import themidibus.listener.SimpleMidiListener;
import themidibus.listener.StandardMidiListener;

/**
 * The MidiBus class provides a simple way to send and receive MIDI within
 * Processing sketches.
 * <p>
 * <h4>Typical Implementation, Simple</h4>
 * <p>
 * A typical simple Processing MIDI application would begin by invoking the
 * static method {@link #list()} to learn what devices are available. Then using
 * that information a new MidiBus object would be instantiated with with the
 * desired MIDI input and/or output devices. The Processing sketch could then
 * send midi via MidiBus's outgoing methods such as
 * {@link #sendNoteOn(int channel, int pitch, int velocity)},
 * {@link #sendNoteOff(int channel, int pitch, int velocity)} and
 * {@link #sendControllerChange(int channel, int number, int value)} and receive
 * midi via the PApplet methods this package provides support for such as
 * {@link PApplet#noteOn(int channel, int pitch, int velocity)},
 * {@link PApplet#noteOff(int channel, int pitch, int velocity)} and
 * {@link PApplet#controllerChange(int channel, int number, int value)}.
 * <h4>Typical Implementation, Advanced</h4>
 * <p>
 * If you wish to build more complex Processing MIDI applications you can add
 * more input and output devices to any given instance of MidiBus via the
 * addInput() and addOutput() methods. However it is important to understand
 * that each MidiBus object acts like 2 MIDI buses, one for input and one for
 * output. This means, that by design, outgoing MIDI messages are sent to
 * <i>all</i> output devices connected to a given instance of MidiBus, and
 * incomming messages from <i>all</i> input devices connected to a given
 * instance of MidiBus are <i>merged</i> upon reception. In practice, this means
 * that, by design, you cannot tell which of the devices connected to a given
 * instance of MidiBus sent a particular message, nor can you send a MIDI
 * message to one particular device connected to that object. Instead, for
 * independent reception/transmission to different <i>sets</i> of MIDI devices,
 * you can instantiate more than one MidiBus object inside your Processing
 * sketch. Each instance of MidiBus will only send MIDI messages to output
 * devices which are connected to it and inbound MIDI messages arriving at each
 * MidiBus can be diferentiated using the the {@link PApplet} methods with the
 * bus_name parameter.
 * 
 * @version 006
 * @author Severin Smith
 * @see PApplet
 * @see MidiListener
 * @see RawMidiListener
 * @see StandardMidiListener
 * @see SimpleMidiListener
 */
public class MidiBus {

	String bus_name;

	List<InputDeviceContainer> input_devices;
	List<OutputDeviceContainer> output_devices;

	Set<MidiListener> listeners;

	

	

	/* -- Constructors -- */

	/**
	 * Constructs a new Midibus. The new Midibus's bus_name will be generated
	 * automatically.
	 * 
	 * @see #addInput(int device_num)
	 * @see #addInput(String device_name)
	 * @see #addOutput(int device_num)
	 * @see #addOutput(String device_name)
	 * @see #list()
	 */
	public MidiBus() {
		input_devices = new LinkedList<InputDeviceContainer>();
		output_devices = new LinkedList<OutputDeviceContainer>();
		listeners = new HashSet<MidiListener>();
		resetTime();
	}

	


	/* -- Input/Output Handling -- */

	/**
	 * Returns the names of all the attached input devices.
	 * 
	 * @return the names of the attached inputs.
	 * @see #attachedOutputs()
	 */
	public String[] attachedInputs() {
		MidiDevice.Info[] devices_info = attachedInputsMidiDeviceInfo();
		String[] devices = new String[devices_info.length];

		for (int i = 0; i < devices_info.length; i++) {
			devices[i] = devices_info[i].getName();
		}

		return devices;
	}

	/**
	 * Returns the names of all the attached output devices.
	 * 
	 * @return the names of the attached outputs.
	 * @see #attachedInputs()
	 */
	public String[] attachedOutputs() {
		MidiDevice.Info[] devices_info = attachedOutputsMidiDeviceInfo();
		String[] devices = new String[devices_info.length];

		for (int i = 0; i < devices_info.length; i++) {
			devices[i] = devices_info[i].getName();
		}

		return devices;
	}

	/**
	 * Returns the MidiDevice.Info of all the attached input devices.
	 * 
	 * @return the MidiDevice.Info of the attached inputs.
	 */
	MidiDevice.Info[] attachedInputsMidiDeviceInfo() {
		MidiDevice.Info[] devices = new MidiDevice.Info[input_devices.size()];

		for (int i = 0; i < input_devices.size(); i++) {
			devices[i] = input_devices.get(i).info;
		}

		return devices;
	}

	/**
	 * Returns the MidiDevice.Info of all the attached output devices.
	 * 
	 * @return the MidiDevice.Info of the attached outputs.
	 */
	MidiDevice.Info[] attachedOutputsMidiDeviceInfo() {
		MidiDevice.Info[] devices = new MidiDevice.Info[output_devices.size()];

		for (int i = 0; i < output_devices.size(); i++) {
			devices[i] = output_devices.get(i).info;
		}

		return devices;
	}

	/**
	 * Adds a new MIDI input device specified by the index device_num. If the
	 * MIDI input device has already been added, it will not be added again.
	 * 
	 * @param device_num
	 *            the index of the MIDI input device to be added.
	 * @return true if and only if the input device was successfully added.
	 * @see #addInput(String device_name)
	 * @see #list()
	 */
	public boolean addInput(int device_num) {
		if (device_num < 0 )
			return false;

		MidiDevice.Info[] devices = availableInputsMidiDeviceInfo();

		if (device_num >= devices.length) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: The chosen input device numbered [" + device_num + "] was not added because it doesn't exist");
			return false;
		}

		return addInput(devices[device_num]);
	}

	/**
	 * Removes the MIDI input device specified by the index device_num.
	 * 
	 * @param device_num
	 *            the index of the MIDI input device to be removed.
	 * @return true if and only if the input device was successfully removed.
	 * @see #removeInput(String device_name)
	 * @see #attachedInputs()
	 */
	public synchronized boolean removeInput(int device_num) {
		try {
			InputDeviceContainer container = input_devices.get(device_num);

			input_devices.remove(container);

			container.transmitter.close();
			container.receiver.close();

			return true;
		} catch (ArrayIndexOutOfBoundsException e) {
			return false;
		}
	}

	/**
	 * Adds a new MIDI input device specified by the name device_name. If the
	 * MIDI input device has already been added, it will not be added again.
	 * <p>
	 * If two or more MIDI inputs have the same name, whichever appears first
	 * when {@link #list()} is called will be added. If this behavior is
	 * problematic use {@link #addInput(int device_num)} instead.
	 * 
	 * @param device_name
	 *            the name of the MIDI input device to be added.
	 * @return true if and only if the input device was successfully added.
	 * @see #addInput(int device_num)
	 * @see #list()
	 */
	public boolean addInput(String device_name) {
		if (device_name==null||device_name.isEmpty())
			return false;

		MidiDevice.Info[] devices = availableInputsMidiDeviceInfo();

		for (int i = 0; i < devices.length; i++) {
			if (devices[i].getName().equals(device_name))
				return addInput(devices[i]);
			if (devices[i].getDescription().equals(device_name))
				return addInput(devices[i]);
		}

		Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: No available input MIDI devices named: \"" + device_name + "\" were found");
		return false;
	}

	/**
	 * Removes the MIDI input device specified by the name device_name.
	 * <p>
	 * If two or more attached MIDI inputs have the same name, whichever appears
	 * first when {@link #attachedInputs()} is called will be removed. If this
	 * behavior is problematic use {@link #removeInput(int device_num)} instead.
	 * 
	 * @param device_name
	 *            the name of the MIDI input device to be removed.
	 * @return true if and only if the input device was successfully removed.
	 * @see #removeInput(int device_num)
	 * @see #attachedInputs()
	 */
	public synchronized boolean removeInput(String device_name) {
		for (InputDeviceContainer container : input_devices) {
			if (container.info.getName().equals(device_name)) {
				input_devices.remove(container);

				container.transmitter.close();
				container.receiver.close();

				return true;
			}
		}
		return false;
	}

	/**
	 * Adds a new MIDI input device specified by the MidiDevice.Info
	 * device_info. If the MIDI input device has already been added, it will not
	 * be added again.
	 * 
	 * @param device_info
	 *            the MidiDevice.Info of the MIDI input device to be added.
	 * @return true if and only if the input device was successfully added.
	 */
	synchronized boolean addInput(MidiDevice.Info device_info) {
		try {
			MidiDevice new_device = MidiSystem.getMidiDevice(device_info);

			if (new_device.getMaxTransmitters() == 0) {
				Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: The chosen input device \"" + device_info.getName() + "\" was not added because it is output only");
				return false;
			}

			for (InputDeviceContainer container : input_devices) {
				if (device_info.getName().equals(container.info.getName()))
					return false;
			}

			if (!new_device.isOpen())
				new_device.open();

			MReceiver receiver = new MReceiver();
			Transmitter transmitter = new_device.getTransmitter();
			transmitter.setReceiver(receiver);

			InputDeviceContainer new_container = new InputDeviceContainer(new_device);
			new_container.transmitter = transmitter;
			new_container.receiver = receiver;

			input_devices.add(new_container);

			return true;
		} catch (MidiUnavailableException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: The chosen input device \"" + device_info.getName() + "\" was not added because it is unavailable");
			return false;
		}
	}

	/**
	 * Adds a new MIDI output device specified by the index device_num. If the
	 * MIDI output device has already been added, it will not be added again.
	 * 
	 * @param device_num
	 *            the index of the MIDI output device to be added.
	 * @return true if and only if the output device was successfully added.
	 * @see #addOutput(String device_name)
	 * @see #list()
	 */
	public boolean addOutput(int device_num) {
		if (device_num == -1)
			return false;

		MidiDevice.Info[] devices = availableOutputsMidiDeviceInfo();

		if (device_num >= devices.length || device_num < 0) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: The chosen output device numbered [" + device_num + "] was not added because it doesn't exist");
			return false;
		}

		return addOutput(devices[device_num]);
	}

	/**
	 * Removes the MIDI output device specified by the index device_num.
	 * 
	 * @param device_num
	 *            the index of the MIDI output device to be removed.
	 * @return true if and only if the output device was successfully removed.
	 * @see #removeInput(String device_name)
	 * @see #attachedOutputs()
	 */
	public synchronized boolean removeOutput(int device_num) {
		try {
			OutputDeviceContainer container = output_devices.get(device_num);

			output_devices.remove(container);

			container.receiver.close();

			return true;
		} catch (ArrayIndexOutOfBoundsException e) {
			return false;
		}
	}

	/**
	 * Adds a new MIDI output device specified by the name device_name. If the
	 * MIDI output device has already been added, it will not be added again.
	 * <p>
	 * If two or more MIDI outputs have the same name, whichever appears first
	 * when {@link #list()} is called will be added. If this behavior is
	 * problematic use {@link #addOutput(int device_num)} instead.
	 * 
	 * @param device_name
	 *            the name of the MIDI output device to be added.
	 * @return true if and only if the output device was successfully added.
	 * @see #addOutput(int device_num)
	 * @see #list()
	 */
	public boolean addOutput(String device_name) {
		if (device_name.equals(""))
			return false;

		MidiDevice.Info[] devices = availableOutputsMidiDeviceInfo();

		for (int i = 0; i < devices.length; i++) {
			if (devices[i].getName().equals(device_name))
				return addOutput(devices[i]);
			if (devices[i].getDescription().equals(device_name))
				return addOutput(devices[i]);
		}

		Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: No available input MIDI devices named: \"" + device_name + "\" were found");
		return false;
	}

	/**
	 * Removes the MIDI output device specified by the name device_name.
	 * <p>
	 * If two or more attached MIDI outputs have the same name, whichever
	 * appears first when {@link #attachedOutputs()} is called will be removed.
	 * If this behavior is problematic use {@link #removeOutput(int device_num)}
	 * instead.
	 * 
	 * @param device_name
	 *            the name of the MIDI output device to be removed.
	 * @return true if and only if the output device was successfully removed.
	 * @see #removeOutput(int device_num)
	 * @see #attachedOutputs()
	 */
	public synchronized boolean removeOutput(String device_name) {
		for (OutputDeviceContainer container : output_devices) {
			if (container.info.getName().equals(device_name)) {
				output_devices.remove(container);

				container.receiver.close();

				return true;
			}
		}
		return false;
	}

	/**
	 * Adds a new MIDI output device specified by the MidiDevice.Info
	 * device_info. If the MIDI output device has already been added, it will
	 * not be added again.
	 * 
	 * @param device_info
	 *            the MidiDevice.Info of the MIDI output device to be added.
	 * @return true if and only if the input device was successfully added.
	 */
	synchronized boolean addOutput(MidiDevice.Info device_info) {
		try {
			MidiDevice new_device = MidiSystem.getMidiDevice(device_info);

			if (new_device.getMaxReceivers() == 0) {
				Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: The chosen output device \"" + device_info.getName() + "\" was not added because it is input only");
				return false;
			}

			for (OutputDeviceContainer container : output_devices) {
				if (device_info.getName().equals(container.info.getName()))
					return false;
			}

			if (!new_device.isOpen())
				new_device.open();

			OutputDeviceContainer new_container = new OutputDeviceContainer(new_device);
			new_container.receiver = new_device.getReceiver();

			output_devices.add(new_container);

			return true;
		} catch (MidiUnavailableException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: The chosen output device \"" + device_info.getName() + "\" was not added because it is unavailable");
			return false;
		}
	}

	/**
	 * Closes, clears and disposes of all input related Transmitters and
	 * Receivers.
	 * 
	 * @see #clearOutputs()
	 * @see #clearAll()
	 */
	public synchronized void clearInputs() {
		// We are purposefully not closing devices here, because in some cases
		// that will be slow, and we might want later
		// Also it's broken on MAC
		try {
			for (InputDeviceContainer container : input_devices) {
				container.transmitter.close();
				container.receiver.close();
			}
		} catch (Exception e) {
			Logger.getLogger(getClass().getName()).severe("The MidiBus Warning: Unexpected error during clearInputs()");
		}

		input_devices.clear();
	}

	/**
	 * Closes, clears and disposes of all output related Receivers.
	 * 
	 * @see #clearInputs()
	 * @see #clearAll()
	 */
	public synchronized void clearOutputs() {
		// We are purposefully not closing devices here, because in some cases
		// that will be slow, and we might want later
		// Also it's broken on MAC
		try {
			for (OutputDeviceContainer container : output_devices) {
				container.receiver.close();
			}
		} catch (Exception e) {
			Logger.getLogger(getClass().getName()).severe("The MidiBus Warning: Unexpected error during clearOutputs()");
		}

		output_devices.clear();
	}

	/**
	 * Closes, clears and disposes of all input and output related Transmitters
	 * and Receivers.
	 * 
	 * @see #clearInputs()
	 * @see #clearOutputs()
	 */
	public void clearAll() {
		clearInputs();
		clearOutputs();
	}

	/**
	 * Closes all MidiDevices, should only be called when closing the
	 * application, will interrupt all MIDI I/O. Call publicly from stop(),
	 * close() or dispose()
	 * 
	 * @see #clearOutputs()
	 * @see #clearInputs()
	 * @see #clearAll()
	 * @see #stop()
	 * @see #clear()
	 * @see #dispose()
	 */
	void closeAllMidiDevices() {
		MidiDevice.Info[] available_devices = MidiSystem.getMidiDeviceInfo();
		MidiDevice device;

		for (int i = 0; i < available_devices.length; i++) {
			try {
				device = MidiSystem.getMidiDevice(available_devices[i]);
				if (device.isOpen())
					device.close();
			} catch (MidiUnavailableException e) {
				// Device wasn't available, which is fine since we wanted to
				// close it anyways
			}
		}

	}

	/* -- MIDI Out -- */

	/**
	 * Sends a MIDI message with an unspecified number of bytes. The first byte
	 * should be always be the status byte. If the message is a Meta message of
	 * a System Exclusive message it can have more than 3 byte, otherwise all
	 * extra bytes will be dropped.
	 * 
	 * @param data
	 *            the bytes of the MIDI message.
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendMessage(byte[] data) {
		if ((int) ((byte) data[0] & 0xFF) == MetaMessage.META) {
			MetaMessage message = new MetaMessage();
			try {
				byte[] payload = new byte[data.length - 2];
				System.arraycopy(data, 2, payload, 0, data.length - 2);
				message.setMessage((int) ((byte) data[1] & 0xFF), payload, data.length - 2);
				sendMessage(message);
			} catch (InvalidMidiDataException e) {
				Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
			}
		} else if ((int) ((byte) data[0] & 0xFF) == SysexMessage.SYSTEM_EXCLUSIVE || (int) ((byte) data[0] & 0xFF) == SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE) {
			SysexMessage message = new SysexMessage();
			try {
				message.setMessage(data, data.length);
				sendMessage(message);
			} catch (InvalidMidiDataException e) {
				Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
			}
		} else {
			ShortMessage message = new ShortMessage();
			try {
				if (data.length > 2)
					message.setMessage((int) ((byte) data[0] & 0xFF), (int) ((byte) data[1] & 0xFF), (int) ((byte) data[2] & 0xFF));
				else if (data.length > 1)
					message.setMessage((int) ((byte) data[0] & 0xFF), (int) ((byte) data[1] & 0xFF), 0);
				else
					message.setMessage((int) ((byte) data[0] & 0xFF));
				sendMessage(message);
			} catch (InvalidMidiDataException e) {
				Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
			}
		}
	}

	/**
	 * Sends a MIDI message that takes no data bytes.
	 * 
	 * @param status
	 *            the status byte
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendMessage(int status) {
		ShortMessage message = new ShortMessage();
		try {
			message.setMessage(status);
			sendMessage(message);
		} catch (InvalidMidiDataException e) {
			System.out.println(e);
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
		}
	}

	/**
	 * Sends a MIDI message that takes only one data byte. If the message does
	 * not take data, the data byte is ignored.
	 * 
	 * @param status
	 *            the status byte
	 * @param data
	 *            the data byte
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendMessage(int status, int data) {
		sendMessage(status, data, 0);
	}

	/**
	 * Sends a MIDI message that takes one or two data bytes. If the message
	 * takes only one data byte, the second data byte is ignored; if the message
	 * does not take any data bytes, both data bytes are ignored.
	 * 
	 * @param status
	 *            the status byte.
	 * @param data1
	 *            the first data byte.
	 * @param data2
	 *            the second data byte.
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendMessage(int status, int data1, int data2) {
		ShortMessage message = new ShortMessage();
		try {
			message.setMessage(status, data1, data2);
			sendMessage(message);
		} catch (InvalidMidiDataException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
		}
	}

	/**
	 * Sends a channel message which takes up to two data bytes. If the message
	 * only takes one data byte, the second data byte is ignored; if the message
	 * does not take any data bytes, both data bytes are ignored.
	 * 
	 * @param command
	 *            the MIDI command represented by this message.
	 * @param channel
	 *            the channel associated with the message.
	 * @param data1
	 *            the first data byte.
	 * @param data2
	 *            the second data byte.
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendMessage(int command, int channel, int data1, int data2) {
		ShortMessage message = new ShortMessage();
		try {
			message.setMessage(command, channel, data1, data2);
			sendMessage(message);
		} catch (InvalidMidiDataException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
		}
	}

	private long time_diff;
	public void resetTime(){
		time_diff = System.nanoTime();
	}
	public long getMicrosecondPosition(){
		return (System.nanoTime()-time_diff) / 1000;
	}
	/**
	 * Sends a MidiMessage object.
	 * 
	 * @param message
	 *            the MidiMessage.
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public synchronized void sendMessage(MidiMessage message) {
		if(output_devices.isEmpty()){
			return;
		}
		long time = getMicrosecondPosition();
		for (OutputDeviceContainer container : output_devices) {
			container.receiver.send(message,time);
		}
	}

	/**
	 * Sends a NoteOn message to a channel with the specified pitch and
	 * velocity.
	 * 
	 * @param channel
	 *            the channel associated with the message.
	 * @param pitch
	 *            the pitch associated with the message.
	 * @param velocity
	 *            the velocity associated with the message.
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendNoteOn(int channel, int pitch, int velocity) {
		ShortMessage message = new ShortMessage();
		try {
			message.setMessage(ShortMessage.NOTE_ON, constrain(channel, 0, 15), constrain(pitch, 0, 127), constrain(velocity, 0, 127));
			sendMessage(message);
		} catch (InvalidMidiDataException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
		}
	}

	/**
	 * Sends a NoteOff message to a channel with the specified pitch and
	 * velocity.
	 * 
	 * @param channel
	 *            the channel associated with the message.
	 * @param pitch
	 *            the pitch associated with the message.
	 * @param velocity
	 *            the velocity associated with the message.
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendControllerChange(int channel, int number, int value)
	 */
	public void sendNoteOff(int channel, int pitch, int velocity) {
		ShortMessage message = new ShortMessage();
		try {
			message.setMessage(ShortMessage.NOTE_OFF, constrain(channel, 0, 15), constrain(pitch, 0, 127), constrain(velocity, 0, 127));
			sendMessage(message);
		} catch (InvalidMidiDataException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
		}
	}

	/**
	 * Sends a ControllerChange message to a channel with the specified number
	 * and value.
	 * 
	 * @param channel
	 *            the channel associated with the message.
	 * @param number
	 *            the number associated with the message.
	 * @param value
	 *            the value associated with the message.
	 * @see #sendMessage(byte[] data)
	 * @see #sendMessage(int status)
	 * @see #sendMessage(int status, int data)
	 * @see #sendMessage(int status, int data1, int data2)
	 * @see #sendMessage(int command, int channel, int data1, int data2)
	 * @see #sendMessage(MidiMessage message)
	 * @see #sendNoteOn(int channel, int pitch, int velocity)
	 * @see #sendNoteOff(int channel, int pitch, int velocity)
	 */
	public void sendControllerChange(int channel, int number, int value) {
		ShortMessage message = new ShortMessage();
		try {
			message.setMessage(ShortMessage.CONTROL_CHANGE, constrain(channel, 0, 15), constrain(number, 0, 127), constrain(value, 0, 127));
			sendMessage(message);
		} catch (InvalidMidiDataException e) {
			Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Message not sent, invalid MIDI data");
		}
	}

	/* -- MIDI In -- */

	/**
	 * Notifies all types of listeners of a new MIDI message from one of the
	 * MIDI input devices.
	 * 
	 * @param message
	 *            the new inbound MidiMessage.
	 */
	void notifyListeners(MidiMessage message, long timeStamp) {
		for (MidiListener listener : listeners) {
			notifyListeners(listener, message, timeStamp);
		}
	}

	public static final void notifyListeners(MidiListener listener, MidiMessage message, long timeStamp) {
		byte[] data = message.getMessage();
		/* -- RawMidiListener -- */

		if (listener instanceof RawMidiListener)
			((RawMidiListener) listener).rawMidiMessage(data);

		if (listener instanceof RemoteRawMidiListener)
			try {
				((RemoteRawMidiListener) listener).rawMidiMessage(data, timeStamp);
			} catch (RemoteException e) {
			}

		/* -- SimpleMidiListener -- */

		if (listener instanceof SimpleMidiListener)
			MidiMessageDecoder.deliverSimpleMidiEvent((SimpleMidiListener) listener, data, timeStamp);

		if(listener instanceof ChannelBasedMidiListener){
			MidiMessageDecoder.deliverChannelBasedMidiEvent((ChannelBasedMidiListener) listener, data, timeStamp);
		}
		/* -- StandardMidiListener -- */

		if (listener instanceof StandardMidiListener)
			((StandardMidiListener) listener).midiMessage(message, timeStamp);

	}

	

	/* -- Listener Handling -- */

	/**
	 * Adds a listener who will be notified each time a new MIDI message is
	 * received from a MIDI input device. If the listener has already been
	 * added, it will not be added again.
	 * 
	 * @param listener
	 *            the listener to add.
	 * @return true if and only the listener was sucessfully added.
	 * @see #removeMidiListener(MidiListener listener)
	 * @see #registerParent(Object parent)
	 */
	public boolean addMidiListener(MidiListener listener) {
		return listeners.add(listener);
	}

	/**
	 * Removes a given listener.
	 * 
	 * @param listener
	 *            the listener to remove.
	 * @return true if and only the listener was sucessfully removed.
	 * @see #addMidiListener(MidiListener listener)
	 */
	public boolean removeMidiListener(MidiListener listener) {
		return listeners.remove(listener);
	}

	/* -- Utilites -- */

	/**
	 * It's just convient ... move along...
	 */
	int constrain(int value, int min, int max) {
		if (value > max)
			value = max;
		if (value < min)
			value = min;
		return value;
	}

	/**
	 * Returns the name of this MidiBus.
	 * 
	 * @return the name of this MidiBus.
	 * @see #setBusName(String bus_name)
	 * @see #generateBusName()
	 */
	public String getBusName() {
		return bus_name;
	}

	/**
	 * Changes the name of this MidiBus.
	 * 
	 * @param bus_name
	 *            the new name of this MidiBus.
	 * @see #getBusName()
	 * @see #generateBusName()
	 */
	public void setBusName(String bus_name) {
		this.bus_name = bus_name;
	}

	/**
	 * Generate a name for this MidiBus instance.
	 * 
	 * @see #setBusName(String bus_name)
	 * @see #getBusName()
	 */
	public void generateBusName() {
		bus_name = "MidiBus_" + UUID.randomUUID();
	}

	/* -- Object -- */

	/**
	 * Returns a string representation of the object.
	 * 
	 * @return a string representation of the object.
	 */
	public String toString() {
		String output = "MidiBus: " + bus_name + " [";
		output += input_devices.size() + " input(s), ";
		output += output_devices.size() + " output(s), ";
		output += listeners.size() + " listener(s)]";
		return output;
	}

	/**
	 * Indicates whether some other object is "equal to" this one.
	 * 
	 * @param obj
	 *            the reference object with which to compare.
	 * @return if this object is the same as the obj argument; false otherwise.
	 */
	public boolean equals(Object obj) {
		if (obj instanceof MidiBus) {
			MidiBus midibus = (MidiBus) obj;
			if (!this.getBusName().equals(midibus.getBusName()))
				return false;
			if (!this.input_devices.equals(midibus.input_devices))
				return false;
			if (!this.output_devices.equals(midibus.output_devices))
				return false;
			if (!this.listeners.equals(midibus.listeners))
				return false;
			return true;
		}
		return false;
	}

	/**
	 * Returns a hash code value for the object.
	 * 
	 * @return a hash code value for this object.
	 */
	public int hashCode() {
		return bus_name.hashCode() + input_devices.hashCode() + output_devices.hashCode() + listeners.hashCode();
	}

	/**
	 * Override the finalize() method from java.lang.Object.
	 * 
	 */
	protected void finalize() {
		close();
	}

	/* -- Shutting Down -- */

	/**
	 * Closes this MidiBus and all connections it has with other MIDI devices.
	 * This method exists as per standard javax.sound.midi syntax. It is
	 * functionaly equivalent to stop() and dispose().
	 * 
	 * @see #stop()
	 * @see #dispose()
	 */
	public void close() {
		closeAllMidiDevices();
	}

	/**
	 * Closes this MidiBus and all connections it has with other MIDI devices.
	 * This method exit as per standard Processing syntax for users who are
	 * doing their sketch cleanup themselves using the stop() function. It is
	 * functionaly equivalent to close() and dispose().
	 * 
	 * @see #close()
	 * @see #dispose()
	 */
	public void stop() {
		close();
	}

	/**
	 * Closes this MidiBus and all connections it has with other MIDI devices.
	 * This method exit as per standard Processing library syntax and is called
	 * automatically whenever the parent applet shuts down. It is functionaly
	 * equivalent to close() and stop().
	 * 
	 * @see #close()
	 * @see #stop()
	 */
	public void dispose() {
		close();
	}

	/* -- Static methods -- */

	/**
	 * List all installed MIDI devices. The index, name and type
	 * (input/output/unavailable) of each devices will be indicated.
	 * 
	 * @see #availableInputs()
	 * @see #availableOutputs()
	 * @see #unavailableDevices()
	 */
	static public void list() {
		String[] available_inputs = availableInputs();
		String[] available_outputs = availableOutputs();
		String[] unavailable = unavailableDevices();

		if (available_inputs.length == 0 && available_outputs.length == 0 && unavailable.length == 0)
			return;

		System.out.println("\nAvailable MIDI Devices:");
		if (available_inputs.length != 0) {
			System.out.println("----------Input----------");
			for (int i = 0; i < available_inputs.length; i++)
				System.out.println("[" + i + "] \"" + available_inputs[i] + "\"");
		}
		if (available_outputs.length != 0) {
			System.out.println("----------Output----------");
			for (int i = 0; i < available_outputs.length; i++)
				System.out.println("[" + i + "] \"" + available_outputs[i] + "\"");
		}
		if (unavailable.length != 0) {
			System.out.println("----------Unavailable----------");
			for (int i = 0; i < unavailable.length; i++)
				System.out.println("[" + i + "] \"" + unavailable[i] + "\"");
		}
	}

	/**
	 * Returns the names of all the available input devices.
	 * 
	 * @return the names of the available inputs.
	 * @see #list()
	 * @see #availableOutputs()
	 * @see #unavailableDevices()
	 */
	static public String[] availableInputs() {
		MidiDevice.Info[] devices_info = availableInputsMidiDeviceInfo();
		String[] devices = new String[devices_info.length];

		for (int i = 0; i < devices_info.length; i++) {
			devices[i] = devices_info[i].getName();
		}

		return devices;
	}

	/**
	 * Returns the names of all the available output devices.
	 * 
	 * @return the names of the available outputs.
	 * @see #list()
	 * @see #availableInputs()
	 * @see #unavailableDevices()
	 */
	static public String[] availableOutputs() {
		MidiDevice.Info[] devices_info = availableOutputsMidiDeviceInfo();
		String[] devices = new String[devices_info.length];

		for (int i = 0; i < devices_info.length; i++) {
			devices[i] = devices_info[i].getName();
		}

		return devices;
	}

	/**
	 * Returns the names of all the unavailable devices.
	 * 
	 * @return the names of the unavailable devices.
	 * @see #list()
	 * @see #availableInputs()
	 * @see #availableOutputs()
	 */
	static public String[] unavailableDevices() {
		MidiDevice.Info[] devices_info = unavailableMidiDeviceInfo();
		String[] devices = new String[devices_info.length];

		for (int i = 0; i < devices_info.length; i++) {
			devices[i] = devices_info[i].getName();
		}

		return devices;
	}

	/**
	 * Returns the MidiDevice.Info of all the available input devices.
	 * 
	 * @return the MidiDevice.Info of the available inputs.
	 */
	static MidiDevice.Info[] availableInputsMidiDeviceInfo() {
		MidiDevice.Info[] available_devices = MidiSystem.getMidiDeviceInfo();
		MidiDevice device;

		Vector<MidiDevice.Info> devices_list = new Vector<MidiDevice.Info>();

		for (int i = 0; i < available_devices.length; i++) {
			try {
				device = MidiSystem.getMidiDevice(available_devices[i]);
				// This open close checks to make sure the announced device is
				// truely available
				// There are many reports on Windows that some devices lie about
				// their availability
				// (For instance the Microsoft GS Wavetable Synth)
				// But in theory I guess this could happen on any OS, so I'll
				// just do it all the time.
				if (!device.isOpen()) {
					device.open();
					device.close();
				}
				if (device.getMaxTransmitters() != 0)
					devices_list.add(available_devices[i]);
			} catch (MidiUnavailableException e) {
				// Device was unavailable which is fine, we only care about
				// available inputs
			}
		}

		MidiDevice.Info[] devices = new MidiDevice.Info[devices_list.size()];

		devices_list.toArray(devices);

		return devices;
	}

	/**
	 * Returns the MidiDevice.Info of all the available output devices.
	 * 
	 * @return the MidiDevice.Info of the available output.
	 */
	static MidiDevice.Info[] availableOutputsMidiDeviceInfo() {
		MidiDevice.Info[] available_devices = MidiSystem.getMidiDeviceInfo();
		MidiDevice device;

		Vector<MidiDevice.Info> devices_list = new Vector<MidiDevice.Info>();

		for (int i = 0; i < available_devices.length; i++) {
			try {
				device = MidiSystem.getMidiDevice(available_devices[i]);
				// This open close checks to make sure the announced device is
				// truely available
				// There are many reports on Windows that some devices lie about
				// their availability
				// (For instance the Microsoft GS Wavetable Synth)
				// But in theory I guess this could happen on any OS, so I'll
				// just do it all the time.
				if (!device.isOpen()) {
					device.open();
					device.close();
				}
				if (device.getMaxReceivers() != 0)
					devices_list.add(available_devices[i]);
			} catch (MidiUnavailableException e) {
				// Device was unavailable which is fine, we only care about
				// available output
			}
		}

		MidiDevice.Info[] devices = new MidiDevice.Info[devices_list.size()];

		devices_list.toArray(devices);

		return devices;
	}

	/**
	 * Returns the MidiDevice.Info of all the unavailable devices.
	 * 
	 * @return the MidiDevice.Info of the unavailable devices.
	 */
	static MidiDevice.Info[] unavailableMidiDeviceInfo() {
		MidiDevice.Info[] available_devices = MidiSystem.getMidiDeviceInfo();
		MidiDevice device;

		Vector<MidiDevice.Info> devices_list = new Vector<MidiDevice.Info>();

		for (int i = 0; i < available_devices.length; i++) {
			try {
				device = MidiSystem.getMidiDevice(available_devices[i]);
				// This open close checks to make sure the announced device is
				// truely available
				// There are many reports on Windows that some devices lie about
				// their availability
				// (For instance the Microsoft GS Wavetable Synth)
				// But in theory I guess this could happen on any OS, so I'll
				// just do it all the time.
				if (!device.isOpen()) {
					device.open();
					device.close();
				}
			} catch (MidiUnavailableException e) {
				devices_list.add(available_devices[i]);
			}
		}

		MidiDevice.Info[] devices = new MidiDevice.Info[devices_list.size()];

		devices_list.toArray(devices);

		return devices;
	}

	/* -- Nested Classes -- */

	private class MReceiver implements Receiver {

		MReceiver() {

		}

		public void close() {

		}

		public void send(MidiMessage message, long timeStamp) {

			if (message.getStatus() == ShortMessage.NOTE_ON && message.getMessage()[2] == 0) {
				try {
					ShortMessage tmp_message = (ShortMessage) message;
					tmp_message.setMessage(ShortMessage.NOTE_OFF, tmp_message.getData1(), tmp_message.getData2());
					message = tmp_message;
				} catch (Exception e) {
					Logger.getLogger(getClass().getName()).severe("\nThe MidiBus Warning: Mystery error during noteOn (0 velocity) to noteOff conversion");
				}
			}

			notifyListeners(message, getMicrosecondPosition());
			sendMessage(message);
		}

	}

	private class InputDeviceContainer {

		MidiDevice.Info info;

		Transmitter transmitter;
		Receiver receiver;

		InputDeviceContainer(MidiDevice device) {
			this.info = device.getDeviceInfo();
		}

		public boolean equals(Object container) {
			if (container instanceof InputDeviceContainer && ((InputDeviceContainer) container).info.getName().equals(this.info.getName()))
				return true;
			else
				return false;
		}

		public int hashCode() {
			return info.getName().hashCode();
		}

	}

	private class OutputDeviceContainer {

		MidiDevice.Info info;

		Receiver receiver;

		OutputDeviceContainer(MidiDevice device) {
			this.info = device.getDeviceInfo();
		}

		public boolean equals(Object container) {
			if (container instanceof OutputDeviceContainer && ((OutputDeviceContainer) container).info.getName().equals(this.info.getName()))
				return true;
			else
				return false;
		}

		public int hashCode() {
			return info.getName().hashCode();
		}

	}

}